import { isNumber } from 'lodash-es';

interface ICache {
  readonly size: number;
  getItem: <T = unknown>(key: string) => Promise<T | null>;
  hasItem: (key: string) => Promise<boolean>;
  setItem: <T = any>(
    key: string,
    value: T,
    opts?: Partial<Exclude<ICacheItemMeta, 'createdAt'>>
  ) => Promise<void>;
  removeItem: (key: string) => Promise<void>;
  clear: () => Promise<void>;
}

interface ICacheItemMeta {
  createdAt: number;
  expiredAt?: /* 指定一个过期时间 */ number;
}

interface ICacheItem<T = unknown> {
  readonly key: string;
  readonly value: T;
  readonly meta: ICacheItemMeta;
}

class WebStorageCache implements ICache {
  private readonly prefix: string = 'unis-';
  private readonly items: Map<string, ICacheItem<any>> = new Map<string, ICacheItem<any>>();
  private readonly storage: Storage = window.localStorage;

  private restore(): void {
    for (let i = 0; i < localStorage.length; i++) {
      const key = localStorage.key(i);
      if (!key) {
        continue;
      }

      if (key.startsWith(this.prefix)) {
        const item = localStorage.getItem(key);
        if (item) {
          this.items.set(key, JSON.parse(item));
        }
      }
    }
  }

  constructor(
    inits: Partial<{ prefix: string; storage: Storage }> = {
      prefix: 'unis-',
      storage: window.localStorage,
    }
  ) {
    const { prefix = 'unis-', storage = window.localStorage } = inits;

    this.prefix = prefix;
    this.storage = storage;

    this.restore();
  }

  get size(): number {
    return this.items.size;
  }

  private _getItem<T = unknown>(key: string): ICacheItem<T> | null {
    try {
      if (this.items.has(key)) {
        return this.items.get(key) as ICacheItem<T>;
      }

      const json = this.storage.getItem(key);
      if (json) {
        return JSON.parse(json) as ICacheItem<T>;
      }
    } catch (error) {
      return null;
    }

    return null;
  }

  async getItem<T = unknown>(key: string): Promise<T | null> {
    const realKey = `${this.prefix}${key}`;
    const item = this._getItem<T>(realKey);
    if (!item) {
      return Promise.resolve(null);
    }

    const expiredAt = item.meta.expiredAt;
    const now = Date.now();
    if (!isNumber(expiredAt)) {
      return Promise.resolve(item.value);
    }

    if (isNumber(expiredAt) && expiredAt > now) {
      return Promise.resolve(item.value);
    }

    return Promise.resolve(null);
  }

  async hasItem(key: string): Promise<boolean> {
    const realKey = `${this.prefix}${key}`;
    if (this.items.has(realKey)) {
      return Promise.resolve<boolean>(this.items.has(realKey));
    }
    this.restore();
    return Promise.resolve<boolean>(this.items.has(realKey));
  }

  async setItem<T = any>(
    key: string,
    value: T,
    opts: Partial<Exclude<ICacheItemMeta, 'createdAt'>> = {}
  ): Promise<void> {
    const meta: ICacheItemMeta = {
      createdAt: Date.now(),
    };

    if (Number.isInteger(opts.expiredAt)) {
      meta.expiredAt = opts.expiredAt;
    }

    const realKey = `${this.prefix}${key}`;
    const item = {
      key: realKey,
      value,
      meta,
    };
    this.items.set(realKey, item);
    this.storage.setItem(realKey, JSON.stringify(item));
  }

  async removeItem(key: string): Promise<void> {
    const realKey = `${this.prefix}${key}`;
    this.items.delete(realKey);
    this.storage.removeItem(realKey);
  }

  async clear(): Promise<void> {
    this.items.clear();
    this.storage.clear();
  }
}

const cache = new WebStorageCache({
  prefix: 'item-client-web/',
  storage: window.localStorage,
});
const session = new WebStorageCache({
  prefix: 'item-client-web/',
  storage: window.sessionStorage,
});

export { WebStorageCache, cache, session };

export type { ICache, ICacheItem };
